ReadDirectoryChangesW 无法检测和处理监视目录的删除
ReadDirectoryChangesW unable to detect and handle deletion of watched directory
我尝试监视目录树的内容,其中包含大量文件(例如,许多目录每个目录有 9000 个文件)。
Synchron mode:
我首先尝试在阻塞模式(同步)下使用 ReadDirectoryChangesW,但是当我删除监视的目录时,我最终陷入无法检测或退出的死锁。
#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):
print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))
# File monitor
FILE_LIST_DIRECTORY = 0x0001
buffer = win32file.AllocateReadBuffer(1024 * 64)
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS,
None)
# Monitor directory for changes
while not self._shutdown.is_set():
# Create handle to directory if missing
#if os.path.isdir(self.path):
self.fh.write("ReOpen Exists {0}\n".format(os.path.isdir(self.path)))
self.fh.flush()
try:
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS,
None)
except:
self.fh.write("Handle is dead\n")
self.fh.flush()
try:
self.fh.write("{0}\n".format(newH))
self.fh.flush()
except:
self.fh.write("Write failed\n")
self.fh.flush()
self.fh.write("Check Changes\n")
self.fh.flush()
results = win32file.ReadDirectoryChangesW(hDir,
1024 * 64,
True,
win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
win32con.FILE_NOTIFY_CHANGE_SIZE |
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
win32con.FILE_NOTIFY_CHANGE_SECURITY,
None,
None)
# Add all changes to queue
for action, file in results:
self.fh.write("Action: {0} on {1}\n".format(action, file))
out_queue.put((action, time.time(), os.path.join(self.path, file)))
self.fh.flush()
#else:
# Done main loop
print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))
当监视目录被删除时,似乎没有办法避免调用阻塞?
此外,由于该函数在一个线程中 运行,我无法在死锁时从一个 "supervisor" 线程中终止它,该线程会监视父目录以对监视目录执行 DELETE 操作,但我没有真的很喜欢这是一个很好的解决方案,因为它涉及更多的代码。
ASynchron mode:
然后我尝试了重叠模式(异步),它不会在死锁中阻塞,但我无法检测到目录句柄何时随着 diorectory 被删除而变为无效。
WaitForSingleObject 调用超时,用 os.path.isdir 检查目录是否存在无济于事,因为如果在同时,它不会 return False,但旧目录句柄仍然无效,并且不会检测新创建的同名目录中的更改。
经过几天尝试各种方法,我终于得到了这段代码,但是它不能完美地工作,因为它仍然没有检测到监视目录的删除,并且在快速大量删除文件时它也确实遗漏了一些文件.同步模式没有的东西。
#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):
print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))
# File monitor
FILE_LIST_DIRECTORY = 0x0001
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = win32event.CreateEvent(None, False, 0, None)
buffer = win32file.AllocateReadBuffer(1024 * 64)
# Main loop to keep watching active
while not self._shutdown.is_set():
# Open directory
try:
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS | win32con.FILE_FLAG_OVERLAPPED,
None)
except:
# Wait before retry
time.sleep(1)
else:
# Monitor directory for changes
while not self._shutdown.is_set():
win32file.ReadDirectoryChangesW(hDir,
buffer,
True,
win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
win32con.FILE_NOTIFY_CHANGE_SIZE |
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
win32con.FILE_NOTIFY_CHANGE_SECURITY,
overlapped,
None)
# Wait for the changes
rc = win32event.WaitForSingleObject(overlapped.hEvent, 10000)
if rc == win32event.WAIT_OBJECT_0:
try:
bytes_returned = win32file.GetOverlappedResult(hDir, overlapped, True)
except:
raise Exception("Error: handle invalid?")
else:
# Get the changes
for action, file in win32file.FILE_NOTIFY_INFORMATION(buffer, bytes_returned):
out_queue.put((action, time.time(), os.path.join(self.path, file)))
elif rc == win32event.WAIT_TIMEOUT:
print("Monitoring instance \'{0}\': Timeout, no actions")
else:
raise Exception("Error?! RC = {0}".format(rc))
# Done main loop
print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))
有没有办法处理检测到监视目录的删除,而不是仅仅删除 win32con.FILE_SHARE_DELETE 标志?
注意事项
- 1st,我请你阅读[SO]:win32file.ReadDirectoryChangesW没有找到所有移动的文件
(@CristiFati 的回答),关于 [ActiveState.Docs]: win32file.ReadDirectoryChangesW (which wraps [MS.Docs]: ReadDirectoryChangesW function)的一般考虑。我将把其他答案的一些方面放在这里(那些非常重要的)
- 始终以异步为目标,因为它允许在超时时正常退出(异常中断可能会保持打开的资源锁定,直到程序退出(有时甚至超越!))
- 不要使用线程,因为 GIL ([Python.Wiki]: GlobalInterpreterLock)
现在,关于 FILE_SHARE_DELETE 的几句话(可以在 [MS.Docs]: CreateFileW function 上找到一些关于它的文档):
黄金法则(或不变法则,如果你愿意的话)是用户不能真的删除文件(或目录) 具有打开的句柄.
尝试删除或重命名(这似乎与当前问题无关,但事实并非如此)带有打开句柄的 dir 可能会产生不同的结果(取决于删除的方式)句柄已创建,API 用于重命名/删除 dir):
- 错误 (ERROR_ACCESS_DENIED) - 当未指定 FILE_SHARE_DELETE 时发生(以及其他一些情况)
- 没有错误,但目录仍然存在 - 一般意味着它被安排删除,一旦它的上次打开句柄关闭[=139]就会自动消失=]
- 成功,目录被删除。实际上,这不是真的,dir 只是移动(重命名)为“RECYCLE.BIN”(试图从那里删除它会导致 #1.; 所以会尝试 really 在 1[=101= 中删除它]st 放置 (Shift + Del 来自 Explorer))
我测试了上述场景,试图以各种方式删除/重命名 dir:
- cmd:
rmdir /q /s
, move /y
- Explorer: Del, Shift + Del
- Windows指挥官: F8, Shift + F8
我开始研究解决您的问题的方法,然后遇到了 [MS.Docs]: GetFinalPathNameByHandleW function (win32file.GetFinalPathNameByHandle)。玩过:
>>> import sys
>>> import os
>>> import win32api
>>> import win32file
>>> import win32con
>>>
>>> print("Python {:s} on {:s}\n".format(sys.version, sys.platform))
Python 3.5.4 (v3.5.4:3f56838, Aug 8 2017, 02:17:05) [MSC v.1900 64 bit (AMD64)] on win32
>>> os.listdir()
['code00.py', 'test']
>>> test_dir = ".\test"
>>> os.path.abspath(test_dir)
'e:\Work\Dev\Whosebug\q049652110\test'
>>> h = win32file.CreateFile(test_dir, win32con.GENERIC_READ, win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE, None, win32con.OPEN_EXISTING, win32con.FILE_FLAG_BACKUP_SEMANTICS, None)
>>> h
<PyHANDLE:620>
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\Work\Dev\Whosebug\q049652110\test'
>>> test_dir1 = test_dir + "1"
>>> os.rename(test_dir, test_dir1)
>>> os.listdir()
['code00.py', 'test1']
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\Work\Dev\Whosebug\q049652110\test1'
>>> os.rename(test_dir1, test_dir)
>>> os.listdir()
['code00.py', 'test']
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\Work\Dev\Whosebug\q049652110\test'
>>> os.unlink(test_dir)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
PermissionError: [WinError 5] Access is denied: '.\test'
>>> # Delete the dir from elsewhere (don't use os.rmdir since that will only schedule the dir for deletion)
...
>>> os.listdir()
['code00.py']
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\$RECYCLE.BIN\S-1-5-21-1906798797-2830956273-3148971768-1002\$RY7SH8D'
>>> os.mkdir(test_dir)
>>> os.listdir()
['code00.py', 'test']
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\$RECYCLE.BIN\S-1-5-21-1906798797-2830956273-3148971768-1002\$RY7SH8D'
>>> os.rmdir(test_dir) # Since the new "test" dir wasn't open, operation successful
>>> os.listdir()
['code00.py']
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\$RECYCLE.BIN\S-1-5-21-1906798797-2830956273-3148971768-1002\$RY7SH8D'
>>> # Restore the dir from RECYCLE.BIN
...
>>> os.listdir()
['code00.py', 'test']
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\Work\Dev\Whosebug\q049652110\test'
>>> os.rmdir(test_dir) # Still an open handle, scheduled to be deleted
>>> os.listdir()
['code00.py', 'test']
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
'\\?\E:\Work\Dev\Whosebug\q049652110\test'
>>> win32api.CloseHandle(h)
>>> os.listdir()
['code00.py'] # After closing the handle the dir was deleted
>>> h
<PyHANDLE:0>
>>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
pywintypes.error: (6, 'GetFinalPathNameByHandle', 'The handle is invalid.')
注意:我也试过[MS.Docs]: GetFileInformationByHandle function(win32file.GetFileInformationByHandle),但无法重现行为,甚至没有 3 个 pywintypes.datetime 字段之一(应该是上次访问/修改时间);重命名/删除 dir 时,信息的 none 发生了变化。我没有花时间去调查,我想到了2个可能的原因:
该数据以某种方式存储在 HANDLE“内部”,并且该函数实际上并不查询 FS ] 调用时(相对于 GetFinalPathNameByHandle)
当 dir 被重命名/删除时,那些日期字段为其父 dir(s)
所以,我们似乎有一个赢家。我只打算 post 算法(代码应该相当简单):
- 在CreateFile之后,在句柄上调用GetFinalPathNameByHandle并保存在一个变量中
- 在循环中(不断调用ReadDirectoryChangesW),在每次迭代(WAIT_TIMEOUT)调用GetFinalPathNameByHandle 再次在句柄上并将结果与保存的值进行比较;如果它们不同(dir 被重命名/“删除”),则采取适当的措施,例如:
- 跳出循环
- 重新打开(重新创建)目录并重新开始
- 让用户知道
其他可能的方法(尽管不受欢迎):
- 不要指定 FILE_SHARE_DELETE(如您所述),这样 dir 将是 不可重命名 / 不可删除
- 监控父级dir(如ReadDirectoryChangesW):
Retrieves information that describes the changes within the specified directory. The function does not report changes to the specified directory itself.
- 由于父 dir 可能包含您不关心的其他 dirs/文件,我想到了一个(蹩脚的) 解决方法 (gainarie) 可能有效也可能无效:
- 选择一个目录(与我们谈论的完全不同)
- 为我们的dir创建一个symlink来监控内部(你控制那个dir, 别人不会乱来的)
- 监控它(这里我既不知道事件是否通过symlink转发,也不知道中断link时监控如何反应)
关于“事件丢失”,正如我在另一个答案中指出的那样,没有办法确保所有这些都会被处理,只有尽量减少丢失的数量的方法。
好的记录:CristiFati 的解决方案(和对问题的解释)成功了。
实际上有 2 个问题:
当简单地删除文件时,它已被移至回收站,因此句柄实际上仍然有效,但更改的位置不再被跟踪。使用 GetFinalPathNameByHandle
成功了!我可以检测到删除,并进行相应处理。
其次,当删除文件而不去回收站时,出现错误,因为该文件在文件夹中仍然可见但无法访问,因此随后对 CreateFile
的调用失败并出现拒绝访问错误。这是因为之前的文件句柄仍处于打开状态,因此删除尚未完成,只是在排队。所以现在我尝试在调用 CreateFile
.
之前关闭任何打开的句柄
下面是工作代码:
#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):
print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))
# File monitor
FILE_LIST_DIRECTORY = 0x0001
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = win32event.CreateEvent(None, False, 0, None)
buffer = win32file.AllocateReadBuffer(1024 * 64)
# Main loop to keep watching active
while not self._shutdown.is_set():
# Open directory
try:
# Ensure handle is closed so delete event can fire and actually delete the folder
try:
win32file.CloseHandle(hDir)
finally:
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS | win32con.FILE_FLAG_OVERLAPPED,
None)
hDirPath = win32file.GetFinalPathNameByHandle(hDir, win32con.FILE_NAME_NORMALIZED)
except:
print("Directory to monitor does not exist! Waiting...")
# Wait before retry
time.sleep(1)
else:
# Signal initialized event on queue, so the directory can be initialized
out_queue.put((0, time.time(), None))
# Monitor directory for changes
while not self._shutdown.is_set():
win32file.ReadDirectoryChangesW(hDir,
buffer,
True,
win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
win32con.FILE_NOTIFY_CHANGE_SIZE |
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
win32con.FILE_NOTIFY_CHANGE_SECURITY,
overlapped,
None)
# Wait for the changes
rc = win32event.WaitForSingleObject(overlapped.hEvent, 10000)
# Detect move to Recycle bin
try:
if hDirPath != win32file.GetFinalPathNameByHandle(hDir, win32con.FILE_NAME_NORMALIZED):
# Set watched dir as deleted (FileAction: 2 = delete)
out_queue.put( (2, time.time(), self.path) )
# Exits loop and wait for reinit
break;
except:
print("Error: directory removed or invalid handle...")
break;
if rc == win32event.WAIT_OBJECT_0:
try:
bytes_returned = win32file.GetOverlappedResult(hDir, overlapped, True)
except:
print("Error: directory removed or invalid handle...")
break;
else:
# Get the changes
for action, file in win32file.FILE_NOTIFY_INFORMATION(buffer, bytes_returned):
out_queue.put((action, time.time(), os.path.join(self.path, file)))
elif rc == win32event.WAIT_TIMEOUT:
print("Monitoring instance \'{0}\': Timeout, no actions".format(self.name))
else:
print("Error?! RC = {0}".format(rc))
break
# Done main loop
print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))
我尝试监视目录树的内容,其中包含大量文件(例如,许多目录每个目录有 9000 个文件)。
Synchron mode:
我首先尝试在阻塞模式(同步)下使用 ReadDirectoryChangesW,但是当我删除监视的目录时,我最终陷入无法检测或退出的死锁。
#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):
print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))
# File monitor
FILE_LIST_DIRECTORY = 0x0001
buffer = win32file.AllocateReadBuffer(1024 * 64)
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS,
None)
# Monitor directory for changes
while not self._shutdown.is_set():
# Create handle to directory if missing
#if os.path.isdir(self.path):
self.fh.write("ReOpen Exists {0}\n".format(os.path.isdir(self.path)))
self.fh.flush()
try:
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS,
None)
except:
self.fh.write("Handle is dead\n")
self.fh.flush()
try:
self.fh.write("{0}\n".format(newH))
self.fh.flush()
except:
self.fh.write("Write failed\n")
self.fh.flush()
self.fh.write("Check Changes\n")
self.fh.flush()
results = win32file.ReadDirectoryChangesW(hDir,
1024 * 64,
True,
win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
win32con.FILE_NOTIFY_CHANGE_SIZE |
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
win32con.FILE_NOTIFY_CHANGE_SECURITY,
None,
None)
# Add all changes to queue
for action, file in results:
self.fh.write("Action: {0} on {1}\n".format(action, file))
out_queue.put((action, time.time(), os.path.join(self.path, file)))
self.fh.flush()
#else:
# Done main loop
print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))
当监视目录被删除时,似乎没有办法避免调用阻塞?
此外,由于该函数在一个线程中 运行,我无法在死锁时从一个 "supervisor" 线程中终止它,该线程会监视父目录以对监视目录执行 DELETE 操作,但我没有真的很喜欢这是一个很好的解决方案,因为它涉及更多的代码。
ASynchron mode:
然后我尝试了重叠模式(异步),它不会在死锁中阻塞,但我无法检测到目录句柄何时随着 diorectory 被删除而变为无效。 WaitForSingleObject 调用超时,用 os.path.isdir 检查目录是否存在无济于事,因为如果在同时,它不会 return False,但旧目录句柄仍然无效,并且不会检测新创建的同名目录中的更改。
经过几天尝试各种方法,我终于得到了这段代码,但是它不能完美地工作,因为它仍然没有检测到监视目录的删除,并且在快速大量删除文件时它也确实遗漏了一些文件.同步模式没有的东西。
#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):
print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))
# File monitor
FILE_LIST_DIRECTORY = 0x0001
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = win32event.CreateEvent(None, False, 0, None)
buffer = win32file.AllocateReadBuffer(1024 * 64)
# Main loop to keep watching active
while not self._shutdown.is_set():
# Open directory
try:
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS | win32con.FILE_FLAG_OVERLAPPED,
None)
except:
# Wait before retry
time.sleep(1)
else:
# Monitor directory for changes
while not self._shutdown.is_set():
win32file.ReadDirectoryChangesW(hDir,
buffer,
True,
win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
win32con.FILE_NOTIFY_CHANGE_SIZE |
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
win32con.FILE_NOTIFY_CHANGE_SECURITY,
overlapped,
None)
# Wait for the changes
rc = win32event.WaitForSingleObject(overlapped.hEvent, 10000)
if rc == win32event.WAIT_OBJECT_0:
try:
bytes_returned = win32file.GetOverlappedResult(hDir, overlapped, True)
except:
raise Exception("Error: handle invalid?")
else:
# Get the changes
for action, file in win32file.FILE_NOTIFY_INFORMATION(buffer, bytes_returned):
out_queue.put((action, time.time(), os.path.join(self.path, file)))
elif rc == win32event.WAIT_TIMEOUT:
print("Monitoring instance \'{0}\': Timeout, no actions")
else:
raise Exception("Error?! RC = {0}".format(rc))
# Done main loop
print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))
有没有办法处理检测到监视目录的删除,而不是仅仅删除 win32con.FILE_SHARE_DELETE 标志?
注意事项
- 1st,我请你阅读[SO]:win32file.ReadDirectoryChangesW没有找到所有移动的文件 (@CristiFati 的回答),关于 [ActiveState.Docs]: win32file.ReadDirectoryChangesW (which wraps [MS.Docs]: ReadDirectoryChangesW function)的一般考虑。我将把其他答案的一些方面放在这里(那些非常重要的)
- 始终以异步为目标,因为它允许在超时时正常退出(异常中断可能会保持打开的资源锁定,直到程序退出(有时甚至超越!))
- 不要使用线程,因为 GIL ([Python.Wiki]: GlobalInterpreterLock)
现在,关于 FILE_SHARE_DELETE 的几句话(可以在 [MS.Docs]: CreateFileW function 上找到一些关于它的文档):
黄金法则(或不变法则,如果你愿意的话)是用户不能真的删除文件(或目录) 具有打开的句柄.
尝试删除或重命名(这似乎与当前问题无关,但事实并非如此)带有打开句柄的 dir 可能会产生不同的结果(取决于删除的方式)句柄已创建,API 用于重命名/删除 dir):
- 错误 (ERROR_ACCESS_DENIED) - 当未指定 FILE_SHARE_DELETE 时发生(以及其他一些情况)
- 没有错误,但目录仍然存在 - 一般意味着它被安排删除,一旦它的上次打开句柄关闭[=139]就会自动消失=]
- 成功,目录被删除。实际上,这不是真的,dir 只是移动(重命名)为“RECYCLE.BIN”(试图从那里删除它会导致 #1.; 所以会尝试 really 在 1[=101= 中删除它]st 放置 (Shift + Del 来自 Explorer))
我测试了上述场景,试图以各种方式删除/重命名 dir:
- cmd:
rmdir /q /s
,move /y
- Explorer: Del, Shift + Del
- Windows指挥官: F8, Shift + F8
我开始研究解决您的问题的方法,然后遇到了 [MS.Docs]: GetFinalPathNameByHandleW function (win32file.GetFinalPathNameByHandle)。玩过:
>>> import sys >>> import os >>> import win32api >>> import win32file >>> import win32con >>> >>> print("Python {:s} on {:s}\n".format(sys.version, sys.platform)) Python 3.5.4 (v3.5.4:3f56838, Aug 8 2017, 02:17:05) [MSC v.1900 64 bit (AMD64)] on win32 >>> os.listdir() ['code00.py', 'test'] >>> test_dir = ".\test" >>> os.path.abspath(test_dir) 'e:\Work\Dev\Whosebug\q049652110\test' >>> h = win32file.CreateFile(test_dir, win32con.GENERIC_READ, win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE, None, win32con.OPEN_EXISTING, win32con.FILE_FLAG_BACKUP_SEMANTICS, None) >>> h <PyHANDLE:620> >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\Work\Dev\Whosebug\q049652110\test' >>> test_dir1 = test_dir + "1" >>> os.rename(test_dir, test_dir1) >>> os.listdir() ['code00.py', 'test1'] >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\Work\Dev\Whosebug\q049652110\test1' >>> os.rename(test_dir1, test_dir) >>> os.listdir() ['code00.py', 'test'] >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\Work\Dev\Whosebug\q049652110\test' >>> os.unlink(test_dir) Traceback (most recent call last): File "<stdin>", line 1, in <module> PermissionError: [WinError 5] Access is denied: '.\test' >>> # Delete the dir from elsewhere (don't use os.rmdir since that will only schedule the dir for deletion) ... >>> os.listdir() ['code00.py'] >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\$RECYCLE.BIN\S-1-5-21-1906798797-2830956273-3148971768-1002\$RY7SH8D' >>> os.mkdir(test_dir) >>> os.listdir() ['code00.py', 'test'] >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\$RECYCLE.BIN\S-1-5-21-1906798797-2830956273-3148971768-1002\$RY7SH8D' >>> os.rmdir(test_dir) # Since the new "test" dir wasn't open, operation successful >>> os.listdir() ['code00.py'] >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\$RECYCLE.BIN\S-1-5-21-1906798797-2830956273-3148971768-1002\$RY7SH8D' >>> # Restore the dir from RECYCLE.BIN ... >>> os.listdir() ['code00.py', 'test'] >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\Work\Dev\Whosebug\q049652110\test' >>> os.rmdir(test_dir) # Still an open handle, scheduled to be deleted >>> os.listdir() ['code00.py', 'test'] >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) '\\?\E:\Work\Dev\Whosebug\q049652110\test' >>> win32api.CloseHandle(h) >>> os.listdir() ['code00.py'] # After closing the handle the dir was deleted >>> h <PyHANDLE:0> >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED) Traceback (most recent call last): File "<stdin>", line 1, in <module> pywintypes.error: (6, 'GetFinalPathNameByHandle', 'The handle is invalid.')
注意:我也试过[MS.Docs]: GetFileInformationByHandle function(win32file.GetFileInformationByHandle),但无法重现行为,甚至没有 3 个 pywintypes.datetime 字段之一(应该是上次访问/修改时间);重命名/删除 dir 时,信息的 none 发生了变化。我没有花时间去调查,我想到了2个可能的原因:
该数据以某种方式存储在 HANDLE“内部”,并且该函数实际上并不查询 FS ] 调用时(相对于 GetFinalPathNameByHandle)
当 dir 被重命名/删除时,那些日期字段为其父 dir(s)
所以,我们似乎有一个赢家。我只打算 post 算法(代码应该相当简单):
- 在CreateFile之后,在句柄上调用GetFinalPathNameByHandle并保存在一个变量中
- 在循环中(不断调用ReadDirectoryChangesW),在每次迭代(WAIT_TIMEOUT)调用GetFinalPathNameByHandle 再次在句柄上并将结果与保存的值进行比较;如果它们不同(dir 被重命名/“删除”),则采取适当的措施,例如:
- 跳出循环
- 重新打开(重新创建)目录并重新开始
- 让用户知道
其他可能的方法(尽管不受欢迎):
- 不要指定 FILE_SHARE_DELETE(如您所述),这样 dir 将是 不可重命名 / 不可删除
- 监控父级dir(如ReadDirectoryChangesW):
Retrieves information that describes the changes within the specified directory. The function does not report changes to the specified directory itself.
- 由于父 dir 可能包含您不关心的其他 dirs/文件,我想到了一个(蹩脚的) 解决方法 (gainarie) 可能有效也可能无效:
- 选择一个目录(与我们谈论的完全不同)
- 为我们的dir创建一个symlink来监控内部(你控制那个dir, 别人不会乱来的)
- 监控它(这里我既不知道事件是否通过symlink转发,也不知道中断link时监控如何反应)
关于“事件丢失”,正如我在另一个答案中指出的那样,没有办法确保所有这些都会被处理,只有尽量减少丢失的数量的方法。
好的记录:CristiFati 的解决方案(和对问题的解释)成功了。
实际上有 2 个问题:
当简单地删除文件时,它已被移至回收站,因此句柄实际上仍然有效,但更改的位置不再被跟踪。使用 GetFinalPathNameByHandle
成功了!我可以检测到删除,并进行相应处理。
其次,当删除文件而不去回收站时,出现错误,因为该文件在文件夹中仍然可见但无法访问,因此随后对 CreateFile
的调用失败并出现拒绝访问错误。这是因为之前的文件句柄仍处于打开状态,因此删除尚未完成,只是在排队。所以现在我尝试在调用 CreateFile
.
下面是工作代码:
#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):
print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))
# File monitor
FILE_LIST_DIRECTORY = 0x0001
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = win32event.CreateEvent(None, False, 0, None)
buffer = win32file.AllocateReadBuffer(1024 * 64)
# Main loop to keep watching active
while not self._shutdown.is_set():
# Open directory
try:
# Ensure handle is closed so delete event can fire and actually delete the folder
try:
win32file.CloseHandle(hDir)
finally:
hDir = win32file.CreateFile(self.path,
FILE_LIST_DIRECTORY,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS | win32con.FILE_FLAG_OVERLAPPED,
None)
hDirPath = win32file.GetFinalPathNameByHandle(hDir, win32con.FILE_NAME_NORMALIZED)
except:
print("Directory to monitor does not exist! Waiting...")
# Wait before retry
time.sleep(1)
else:
# Signal initialized event on queue, so the directory can be initialized
out_queue.put((0, time.time(), None))
# Monitor directory for changes
while not self._shutdown.is_set():
win32file.ReadDirectoryChangesW(hDir,
buffer,
True,
win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
win32con.FILE_NOTIFY_CHANGE_SIZE |
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
win32con.FILE_NOTIFY_CHANGE_SECURITY,
overlapped,
None)
# Wait for the changes
rc = win32event.WaitForSingleObject(overlapped.hEvent, 10000)
# Detect move to Recycle bin
try:
if hDirPath != win32file.GetFinalPathNameByHandle(hDir, win32con.FILE_NAME_NORMALIZED):
# Set watched dir as deleted (FileAction: 2 = delete)
out_queue.put( (2, time.time(), self.path) )
# Exits loop and wait for reinit
break;
except:
print("Error: directory removed or invalid handle...")
break;
if rc == win32event.WAIT_OBJECT_0:
try:
bytes_returned = win32file.GetOverlappedResult(hDir, overlapped, True)
except:
print("Error: directory removed or invalid handle...")
break;
else:
# Get the changes
for action, file in win32file.FILE_NOTIFY_INFORMATION(buffer, bytes_returned):
out_queue.put((action, time.time(), os.path.join(self.path, file)))
elif rc == win32event.WAIT_TIMEOUT:
print("Monitoring instance \'{0}\': Timeout, no actions".format(self.name))
else:
print("Error?! RC = {0}".format(rc))
break
# Done main loop
print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))